# 搭建前端监控系统

# 前言

有两种做法

  1. 使用开源项目,例如sentry,fundebug等。优点是方便,接入即可,缺点是需要银子。
  2. 自己搭建。通过监听、劫持各种事件,处理信息后上报。

sentry可以自己搭建环境就不用收费,那么先来讲讲sentry在vue中的使用。

# sentry

绝大部分内容来自掘金作者:夜_游神

https://juejin.im/post/5eeaebf2f265da02a87f00bf

# 注册账户

  • 首先注册一个账户(测试玩玩可以用自己的,公司项目还是用公司邮箱吧),注册时组织千万别乱填,这玩意很重要,后面很多地方都需要
  • 注册账户完成后注册项目

img

项目名称也不能乱写后面也有用的,选择自己的语言(vue居然还要搜索,我大VUE为何沦落至此?)

注册完项目后你就做好了前置准备了

# 引入项目

img

img

cnpm install @sentry/browser

cnpm install @sentry/integrations

//在main.js中引入
import Vue from 'vue'import * as Sentry from '@sentry/browser';
import { Vue as VueIntegration } from '@sentry/integrations';

Sentry.init({
    dsn: 'https://32ad19d9b80448f593f1df2833256d23@o406425.ingest.sentry.io/5273959',
    integrations: [new VueIntegration({Vue, attachProps: true})],
 })

这时简单制造个报错,即可在控制台看到sentry的接口上报

img

img

高兴之余仔细一看,这特瞄的都报的啥错?完全看不懂啊?因为正式环境的代码都是进行了丑化,没有.map文件就是报错了你也不知道具体位置。这知道和没知道简直没有区别啊

这时候我们就要进入第二步,将.map文件上传到服务器

# 上传sourceMap

一:

因为公司项目还是vue-cli2,这里就是自己研究的

打开package.json,看build命令,调的是build/build.js

进到build.js中看到配置是从config/index,js及webpack.prod.conf中取得

进config/index,js,发现productionSourceMap,置true即可

来到webpack.prod.conf中,因之前换成了ParallelUglifyPlugin插件进行打包,在其配置中开启sourcemap

new ParallelUglifyPlugin({
  cacheDir: '.cache/',   // 设置缓存路径,不改动的调用缓存,第二次及后面build时提速
  uglifyJS:{
    output: {
      // 最紧凑的输出
      beautify: false,
      // 删除所有的注释
      comments: false,
    },
    // 在UglifyJs删除没有用到的代码时不输出警告
    warnings: false,
    compress: {
      // 删除所有的 `console` 语句,可以兼容ie浏览器
      // drop_console: true,
      // 内嵌定义了但是只用到一次的变量
      collapse_vars: true,
      // 提取出出现多次但是没有定义成变量去引用的静态值
      reduce_vars: true,
    }
  },
  sourceMap: true
}),

往下看,还有webpackConfig得改

var webpackConfig = merge(baseWebpackConfig, {
  module: {
    rules: utils.styleLoaders({
      sourceMap: true,
      extract: true
    })
  },
  devtool: config.build.productionSourceMap ? '#source-map' : false,
  output: {
    path: config.build.assetsRoot,
    filename: utils.assetsPath(`js/[name].[chunkhash].js`),
    chunkFilename: utils.assetsPath(`js/[id].[chunkhash].js`)
  },
  plugins: plugins
})

此处devtool不开启貌似就打包不了map,开启了浏览器又报找不到map文件的warning(map没上传的服务器),应该可以通过限制ip访问解决map泄露问题。

好,npm run build看看是否有map文件生成了。

二:

将打包的文件自动上传的sentry服务器,这时候我们需要一个webpack插件@sentry/webpack-plugin 首先就是安装插件

cnpm i @sentry/webpack-plugin --save-dev

然后在根节点处创建文件.sentryclirc(标准配置文件的命名方式)

[auth]
token=token          // 无需引号

[defaults]
url = https://sentry.io
org = 前面提到的组织名
project = 前面提到的项目名

token获取方式

img

如果没有token就注册一个

img

在config文件夹中新建一个sentry.env.js,带上RELEASE_VERSION,用来标记上传的版本号

const release = "test@1.0.0";
process.env.RELEASE_VERSION = release;
module.exports = {
  NODE_ENV: '"sentry"',
  RELEASE_VERSION: `"${release}"`
}

注意写法,试了'"test@1.0.0"'不行,不知道为啥

换成上面的即可

然后在webpack.prod.conf中引入插件

const SentryCliPlugin = require('@sentry/webpack-plugin')

configureWebpack:{
    plugins:[
        new SentryCliPlugin({
            include: "./dist/", // 作用的文件夹,如果只想js报错就./dist/js
            release: process.env.RELEASE_VERSION, // 一致的版本号
            configFile: "sentry.properties", // 不用改
            ignore: ['node_modules', 'webpack.config.js'],
            urlPrefix: "~/",//这里指的你项目需要观测的文件如果你的项目有publicPath这里加上就对了
         })
    ]
}

在插件初始化的地方也要加上版本号 所以main.js也要改成

Sentry.init({
  dsn: 'XXX',
  integrations: [new VueIntegration({Vue, attachProps: true})],
  release: process.env.RELEASE_VERSION
});

这时打包,需要上传,时间比较久

成功后去sentry上看看

img

这里我们看到这就是我们之前定义的项目名称,点击项目名称

img

这时候我们部署上去(这里测试可以在本地跑个服务,做个简单的报错比如abdc(),就会报abcd不是方法),报个错看看

img

img

可以看到报错的源代码了,真香~

上传了sourseMap后还存在一些问题,比如开发调试的时候,报错也会被收集,那么多错,我看的岂不是头都大了,同时开发时报错不会再显示,就会为开发造成困扰,这次我们着重解决这些问题。

# 正式环境与测试环境区分

先在package.json加条新命令

"build--sentry": "cross-env NODE_ENV=sentry node -max_old_space_size=4096 build/build.js",

在main.js中sentry初始化时区分环境,如果不是正式环境就不在初始化就好;

process.env.NODE_ENV === 'sentry' && Sentry.init({
  dsn: 'https://787297c8c01b45d2a6325edcc3c88275@o413596.ingest.sentry.io/5300418',
  integrations: [new VueIntegration({Vue, attachProps: true})],
  release: process.env.RELEASE_VERSION,
  logErrors: false // 设置为true,错误不但会发给平台,也会在页面正常显示,方便调试(文档有)
})

同时在打包的时候,比如在测试环境,我不需要sourseMap,也不需要把代码上传到服务器(免得服务器上代码太乱),所以在webpack上也要最对应的修改:

if(process.env.NODE_ENV === 'sentry') {
  const SentryCliPlugin = require('@sentry/webpack-plugin')
  plugins.push(
    new SentryCliPlugin({
      include: './dist/', // 作用的文件夹,如果只想js报错就./dist/js
      release: process.env.RELEASE_VERSION, // 一致的版本号
      configFile: 'sentry.properties', // 不用改
      ignore: ['node_modules', 'webpack.config.js'],
      urlPrefix: '~/share/green/',// 去掉根目录的文件路径,不写找不到map文件
    })
  )
}

其中plugins为webpackConfig中的plugins,提成数组,不然没法动态加

同时将上文中开启sourcemap的地方都改为process.env.NODE_ENV === 'sentry'

每次打包后还有个问题,就是map文件不需要上传到服务器,需要删除,手动又太麻烦,有webpack的插件可以只打包js,剔除map,但是还要自己解压一边,很麻烦,于是写了个命令行,直接删除

新建记事本,写上下面代码后,后缀改为cmd即可

del /f /s /q .\dist\*.map

至此已经可以在线上使用了

# 搭建自己的sentry服务器

待续

# 搭建自己的前端监控

export default function errorHandler (Vue) {
  const error = (e) => {
    console.log(22222222, e, navigator.userAgent)
  }
  window.onerror = (msg, url, lineNo, columnNo, e) => {
    if (msg === 'Script error.' || !url || e.type === '__skynet_wrapper__') return
    error(e)
  }
  window.addEventListener('unhandledrejection', (e) => {
    if (isObject(e.reason)) {
      error(e.reason)
    } else {
      error(e.reason)
    }
  })
  // const originAddEventListener = EventTarget.prototype.addEventListener
  // EventTarget.prototype.addEventListener = (type, listener, options) => {
  //   const wrappedListener = (...args) => {
  //     try {
  //       return listener.apply(this, args)
  //     } catch (err) {
  //       throw err
  //     }
  //   }
  //   return originAddEventListener.call(this, type, wrappedListener, options)
  // }
  let vuePlugin = function (_Vue) {
    if (!_Vue || !_Vue.config) {
      return
    }
    const _oldOnError = _Vue.config.errorHandler
    _Vue.config.errorHandler = function (errMsg, vm, info) {
      error(errMsg)
      if (typeof console !== 'undefined' && typeof console.error === 'function') {
        console.error(errMsg)
      }
      if (typeof _oldOnError === 'function') {
        _oldOnError.call(this, errMsg, vm, info)
      }
    }
  }
  vuePlugin(Vue)
  const captureBreadcrumb = function (obj) {
    console.log('obj', obj)
  }
  const _breadcrumbEventHandler = (evtName) => {
    let _lastCapturedEvent = ''
    return function (evt) {
      if (_lastCapturedEvent === evt) {
        return
      }
      _lastCapturedEvent = evt
      let target
      try {
        target = htmlTreeAsString(evt.target)
      } catch (e) {
        target = '<unknown>'
      }
      captureBreadcrumb({
        category: `ui.${evtName}`,
        message: target
      })
    }
  }
  function htmlTreeAsString (target) {
    console.log('target', target)
    return target.outerHTML
  }
  window.addEventListener('click', _breadcrumbEventHandler('click', false))
  // setTimeout(() => {
  //   const p = new Promise((resolve, reject) => reject('出错了'))
  //   p.then(null)
  // }, 1000)

  const nativeAjaxSend = XMLHttpRequest.prototype.send // 首先将原生的方法保存。
  const nativeAjaxOpen = XMLHttpRequest.prototype.open

  XMLHttpRequest.prototype.open = function (mothod, url, ...args) { // 劫持open方法,是为了拿到请求的url
    const xhrInstance = this
    xhrInstance._url = url
    nativeAjaxOpen.apply(this, [mothod, url].concat(args))
  }

  XMLHttpRequest.prototype.send = function (...args) { // 对于ajax请求的监控,主要是在send方法里处理。
    const oldCb = this.onreadystatechange
    // const oldErrorCb = this.onerror
    const xhrInstance = this

    xhrInstance.addEventListener('error', function (e) { // 这里捕获到的error是一个ProgressEvent。e.target 的值为 XMLHttpRequest的实例。当网络错误(ajax并没有发出去)或者发生跨域的时候,会触发XMLHttpRequest的error, 此时,e.target.status 的值为:0,e.target.statusText 的值为:''
      const errorObj = {
        error_msg: 'ajax filed',
        error_stack: JSON.stringify({
          status: e.target.status,
          statusText: e.target.statusText
        }),
        error_native: e
      }

      /* 接下来可以对errorObj进行统一处理 */
      captureBreadcrumb({
        type: 'http',
        category: `xhr`,
        message: errorObj
      })
    })

    xhrInstance.addEventListener('abort', function (e) { // 主动取消ajax的情况需要标注,否则可能会产生误报
      if (e.type === 'abort') {
        xhrInstance._isAbort = true
      }
    })

    this.onreadystatechange = function (...innerArgs) {
      if (xhrInstance.readyState === 4) {
        if (!xhrInstance._isAbort && xhrInstance.status !== 200) { // 请求不成功时,拿到错误信息
          const errorObj = {
            error_msg: JSON.stringify({
              code: xhrInstance.status,
              msg: xhrInstance.statusText,
              url: xhrInstance._url
            }),
            error_stack: '',
            error_native: xhrInstance
          }

          /* 接下来可以对errorObj进行统一处理 */
          captureBreadcrumb({
            type: 'http',
            category: `xhr`,
            message: errorObj
          })
        }
      }
      oldCb && oldCb.apply(this, innerArgs)
    }
    nativeAjaxSend.apply(this, args)
  }

  function _captureUrlChange (lastHref, currentHref) {
    console.log('hrefChange', lastHref, currentHref)
  }
  const oldOnPopState = window.onpopstate
  let lastHref = ''
  window.onpopstate = function (...args) {
    let currentHref = location.href
    _captureUrlChange(lastHref, currentHref)
    lastHref = currentHref
    if (oldOnPopState) {
      oldOnPopState.apply(this, args)
      oldOnPopState.apply(this, args)
    }
  }
  // const consoleList = ['info', 'warn', 'error']
  // let wrapConsoleMethod = (level, callback) => {
  //   const nativeConsole = window.console[level]
  //   window.console[level] = (...args) => {
  //     callback && callback(level, args)
  //     nativeConsole.apply(this, args)
  //   }
  // }
  // let consoleMethodCallback = function (level, ...args) {
  //   captureBreadcrumb({
  //     message: args,
  //     level: level,
  //     category: 'console'
  //   })
  // }
  // consoleList.forEach((level) => {
  //   wrapConsoleMethod(level, consoleMethodCallback)
  // })
}
function isObject (value) {
  const type = typeof value
  return value != null && (type === 'object' || type === 'function')
}
// function isFunction (value) {
//   const type = typeof value
//   return value != null && type === 'function'
// }